﻿module Uno

open Xunit
open Xunit.Extensions
open FsUnit.Xunit

type GameId = int

type Color = 
    | Red
    | Green
    | Blue
    | Yellow

type Value = int

type Card =
    | Digit of Value * Color
    | KickBack of Color

type PlayerCount = int


type Player = 
    {
        Id:int
        Cards:Card list
    }

type Direction =
    | ClockWise
    | CounterClockWise

type Turn = 
    | Next
    | Previous

type Event =
    | GameStarted of GameId * PlayerCount * Card
    | CardPlayed of GameId * Player * Card

type Command =
    | StartGame of GameId * PlayerCount * Card
    | PlayCard of GameId * Player * Card

type State =
    {
        Players: Player list
        GameStarted: bool
        CurrentPlayer: Player
        TopCard: Card
        Direction: Direction
    }

let Empty = 
    { 
        Players = []
        GameStarted = false
        CurrentPlayer = {Id=1; Cards=[]}
        TopCard = Digit(1, Red)
        Direction = ClockWise
    }

exception UnoException of string 

let fail message = raise(UnoException(message))

let decide command state = 
    match command, state with
    | StartGame(_, playerCount, _), _ when playerCount < 3  -> fail "can't play Uno with player count less than 3"
    | StartGame(_, _, _), s when s.GameStarted  -> fail "can't start game twice"
    | PlayCard(_,_,_), s when not s.GameStarted-> fail "can't play card if game not started"
    | PlayCard(_, player,_), s when s.CurrentPlayer <> player -> fail "not your turn"
    | StartGame(gameId, player, card), _-> [GameStarted(gameId, player, card)]
    | PlayCard(gameId, player, card), s -> 
        match card, s.TopCard with
        | Digit(v1, c1), Digit(v2, c2) when v1=v2 || c1=c2 -> [CardPlayed(gameId, player, card)]
        | _ -> fail "bad card"

let apply state event = 
    match event with 
    | GameStarted (gameId, playerCount, card) -> 
        { 
        state with 
            GameStarted=true; 
            TopCard=card; 
            Players = [{Id=1;Cards=[]};{Id=1;Cards=[]};{Id=1;Cards=[]};{Id=1;Cards=[]}] 
        }
    | CardPlayed (gameId, player, card) ->  { state with CurrentPlayer = player }

let newGame topCard playerCount = decide (StartGame(1, playerCount, topCard)) Empty |> List.fold apply Empty

let Given command state = command state

let When command state = decide command state

let Expected expected actual = actual |> should equal expected

[<Fact>]
let ``when start game with 4 players, expected 4 players``()=
    let playersEqual number state = state.Players.Length |> should equal number
    
    decide (StartGame(1, 4, Digit(1, Red))) Empty 
    |> List.fold apply Empty
    |> playersEqual 4

[<Fact>]
let ``fail when it is not player's turn``()=
    let aCard = Digit(2, Red)
    (fun()->
    (
        Given newGame aCard 4
        |> When (PlayCard(2, {Id=2; Cards=[]}, aCard))
        |> ignore
    )) |> should throw typeof<UnoException>

[<Theory>]
[<InlineData(0)>]
[<InlineData(1)>]
[<InlineData(2)>]
let ``fail players are less than 3``(playerCount)=
    let aCard = Digit(2, Red)
    (fun()->
    (
        decide (StartGame(1, playerCount, Digit(1, Red))) Empty
        |> List.fold apply Empty
        |> ignore
    )) |> should throw typeof<UnoException>

[<Fact>]
let ``Given a new game When play card 1 Red expected card played``()=
    let card = Digit(1, Red)
    Given newGame (Digit(2, Red)) 4
    |> When (PlayCard(1, Empty.CurrentPlayer, card))
    |> Expected [CardPlayed(1, Empty.CurrentPlayer, card)]

let ``Play Card with good card`` card=
    decide (StartGame(1, 4, Digit(2, Red))) Empty 
    |> List.fold apply Empty
    |> decide (PlayCard(1, Empty.CurrentPlayer, card))
    |> should equal [CardPlayed(1, Empty.CurrentPlayer, card)]

[<Fact>]
let ``Play Card with same value``()= Digit(2, Yellow) |> ``Play Card with good card``

[<Fact>]
let ``Play Card with same color``()= Digit(3, Red) |> ``Play Card with good card``

[<Fact>]
let ``Fail when Play Bad Card`` =
    (fun()->
    (
        decide (StartGame(1, 4, Digit(2, Red))) Empty 
        |> List.fold apply Empty
        |> decide (PlayCard(1,{Id=1; Cards=[]},Digit(1, Yellow)))
        |> ignore
    )) |> should throw typeof<UnoException>
   
[<Fact>]
let ``Can't play card when game not started``()=
    (fun()->
    (
        []
        |> List.fold apply Empty
        |> decide (PlayCard(1, {Id=1; Cards=[]}, Digit(3, Red)))
        |> ignore
    )) |> should throw typeof<UnoException>

[<Fact>]
let ``Start Game first``()=
    [] 
    |> List.fold apply Empty 
    |> decide (StartGame(1, 4, Digit(3, Red)))
    |> should equal [GameStarted(1, 4, Digit(3, Red))]

[<Fact>]
let ``Start game twice should fail``() = 
    
    (fun()->
    (
        [GameStarted(1,4, Digit(1, Red))]
        |> List.fold apply Empty 
        |> decide (StartGame(1, 4, Digit(3, Red)))
        |> ignore
    )) |> should throw typeof<UnoException>